﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using STDTO.DataTransferObjects.Unit.Tests.Interfaces;
using System.Reflection;
using STDTO.DataTransferObjects.Unit.Tests.Objects;
using STDTO.DataTransferObjects.Unit.Tests.DTOs;
using STDTO.Interfaces;
using System.Collections;
using Microsoft.Practices.Unity;
using STDTO.DataTransferObjects.Base;
using AGT.Infrastructure.DataTransferObjects.Unit.Tests;

namespace STDTO.DataTransferObjects.Unit.Tests
{
    [TestClass()]
    public class DataTransferObjectsTester
    {
        public const string STR_DTO = "DTO";

        private TestContext testContextInstance;

        /// <summary>
        ///Gets or sets the test context which provides
        ///information about and functionality for the current test run.
        ///</summary>
        public TestContext TestContext
        {
            get
            {
                return testContextInstance;
            }
            set
            {
                testContextInstance = value;
            }
        }

        #region Additional test attributes

        private static Random _rand = null;
        // 
        //You can use the following additional attributes as you write your tests:
        //
        //Use ClassInitialize to run code before running the first test in the class
        [ClassInitialize()]
        public static void MyClassInitialize(TestContext testContext)
        {
            _rand = new Random(DateTime.Now.Millisecond);
            RegiterTypes();
        }

        private static void RegiterTypes()
        {

            UnitySingleton.Instance.UnityContainer.RegisterInstance<IDtoInstanceCreator>(new  MyDtoInstanceCreator());
            UnitySingleton.Instance.UnityContainer.RegisterInstance<IUnityContainer>(UnitySingleton.Instance.UnityContainer);

            //concrete types
            UnitySingleton.Instance.UnityContainer.RegisterType<ISimpleObject, SimpleObject>();
            UnitySingleton.Instance.UnityContainer.RegisterType<IObjectContainsSimpleObject, ObjectContainsSimpleObject>();
            UnitySingleton.Instance.UnityContainer.RegisterType<IObjectContainsListOfSimpleObjects, ObjectContainsListOfSimpleObjects>();
            UnitySingleton.Instance.UnityContainer.RegisterType<IObjectContainsCircularReference, ObjectContainsCircularReference>();
            UnitySingleton.Instance.UnityContainer.RegisterType<IDeriveFromSimpleObject, DeriveFromSimpleObject>();
            

            //DTOS
            UnitySingleton.Instance.UnityContainer.RegisterType<ISimpleObject, SimpleObjectDto>(STR_DTO);
            UnitySingleton.Instance.UnityContainer.RegisterType<IObjectContainsSimpleObject, ObjectContainsSimpleObjectDto>(STR_DTO);
            UnitySingleton.Instance.UnityContainer.RegisterType<IObjectContainsListOfSimpleObjects, ObjectContainsListOfSimpleObjectDto>(STR_DTO);
            UnitySingleton.Instance.UnityContainer.RegisterType<IObjectContainsCircularReference, ObjectContainsCircularReferenceDto>(STR_DTO);
            UnitySingleton.Instance.UnityContainer.RegisterType<IDeriveFromSimpleObject, SimpleObjectDeriveDto>(STR_DTO);
            UnitySingleton.Instance.UnityContainer.RegisterType(typeof(IList<>),typeof( UnitySelfTrackingDataTransferObjectCollection<>),STR_DTO);
            
        }
        
        #endregion

        
       

        [TestMethod]
        public void Populate_PopulateSimpleClass_SimpleClassPopulated()
        {
            ObjectBase_PopulateTest<ISimpleObject>(5);
        }


        [TestMethod]
        public void Populate_PopulateObjectContainsSimpleObject_ObjectContainsSimpleObjectPopulated()
        {
            ObjectBase_PopulateTest<IObjectContainsSimpleObject>(11);
        }


        [TestMethod]
        public void Populate_PopulateObjectContainsListOfSimpleObject_ObjectContainsListOfSimpleObjectPopulated()
        {
            ObjectBase_PopulateTest<IObjectContainsListOfSimpleObjects>(11);
        }

        [TestMethod]
        public void Populate_PopulateDeriveFromSimpleObject_SimpleObjectDerivePopulated()
        {
            ObjectBase_PopulateTest<IDeriveFromSimpleObject>(11);
        }

        [TestMethod]
        public void Populate_PopulateObjectContainsCircularReference_ObjectContainsCircularReferencePopulated()
        {
            IObjectContainsCircularReference objectContainsCircularReference = CreateObjectContainsCirculreReference();

            IDtoInstanceCreator dtoCreator = UnitySingleton.Instance.UnityContainer.Resolve<IDtoInstanceCreator>();
            ISelfTrackingDataTransferObject<IObjectContainsCircularReference> dtoObject = dtoCreator.CreateDtoInstance<IObjectContainsCircularReference>(objectContainsCircularReference);

            dtoObject.Populate(objectContainsCircularReference);

            IObjectContainsCircularReference dtoRes = dtoObject as IObjectContainsCircularReference;
            Assert.AreEqual(dtoRes.InnerObjectContainsCircularReference.InnerObjectContainsCircularReference, dtoRes);
        }


        [TestMethod]
        public void ApplyChanges_ApplayChangesOnSimpleClass_ChangesApplied()
        {
            ObjectBase_ApplayChangesTest<ISimpleObject>(5,
                (so => so.Name = "jimi")
                );
        }

        [TestMethod]
        public void ApplyChanges_ApplayChangesOnObjectContainsSimpleObject_ChangesApplied()
        {
            ObjectBase_ApplayChangesTest<IObjectContainsSimpleObject>(11,obj => obj.SimpleObject.Name = "momo");
        }


        [TestMethod]
        public void ApplyChanges_ApplayChangesOnObjectContainsListOfSimpleObject_ChangesApplied()
        {
            ObjectBase_ApplayChangesTest<IObjectContainsListOfSimpleObjects>(11, obj => obj.SimpleObjects[0].Name = "momo");
        }

        [TestMethod]
        public void ApplyChanges_ApplayChangesOnObjectContainsListOfSimpleObjectAddItem_ChangesApplied()
        {
            ObjectBase_ApplayChangesTest<IObjectContainsListOfSimpleObjects>(11, obj => obj.SimpleObjects.Add(new SimpleObjectDto(UnitySingleton.Instance.UnityContainer) {
             Id = 123,
             Name   =  "bobo",
            }));
        }

        [TestMethod]
        public void ApplyChanges_ApplayChangesOnObjectContainsListOfSimpleObjectRemoveItem_ChangesApplied()
        {
            ObjectBase_ApplayChangesTest<IObjectContainsListOfSimpleObjects>(11, obj => obj.SimpleObjects.RemoveAt(0));
        }

        [TestMethod]
        public void ApplyChanges_ApplayChangesOnDeriveFromSimpleObject_ChangesApplied()
        {
            ObjectBase_ApplayChangesTest<IDeriveFromSimpleObject>(11,obj => obj.ExtProperty = "Hey you");
        }


        [TestMethod]
        public void ApplyChanges_ApplayChangesOnObjectContainsCircularReference_ChangesApplied()
        {
            IObjectContainsCircularReference objectContainsCircularReference = CreateObjectContainsCirculreReference();

            IDtoInstanceCreator dtoCreator = UnitySingleton.Instance.UnityContainer.Resolve<IDtoInstanceCreator>();
            ISelfTrackingDataTransferObject<IObjectContainsCircularReference> dtoObject = dtoCreator.CreateDtoInstance<IObjectContainsCircularReference>(objectContainsCircularReference);

            dtoObject.Populate(objectContainsCircularReference);

            ((IObjectContainsCircularReference)dtoObject).Id = 1234;

            dtoObject.ApplyChanges(objectContainsCircularReference);

            IObjectContainsCircularReference dtoRes = dtoObject as IObjectContainsCircularReference;
            Assert.AreEqual(dtoRes.InnerObjectContainsCircularReference.InnerObjectContainsCircularReference, dtoRes);
        }


        [TestMethod]
        public void Clone_CloneSimpleClass_ObjectWasCloned()
        {
            ObjectBase_CloneTest<ISimpleObject>(5, obj => obj.Name = "newName");
        }

        [TestMethod]
        public void Clone_CloneObjectContainsSimpleObject_ObjectWasCloned()
        {
            ObjectBase_CloneTest<IObjectContainsSimpleObject>(11,obj => obj.SimpleObject.Name = "updated name");
        }


        [TestMethod]
        public void Clone_CloneObjectContainsListOfSimpleObjects_ObjectWasCloned()
        {
            ObjectBase_CloneTest<IObjectContainsListOfSimpleObjects>(11,obj => obj.SimpleObjects[0].Name = "updated name");
        }

        [TestMethod]
        public void Clone_CloneObjectContainsListOfSimpleObjectsListItemWasAdded_ObjectWasCloned()
        {
            ObjectBase_CloneTest<IObjectContainsListOfSimpleObjects>(11, obj => obj.SimpleObjects.Add(new SimpleObjectDto(UnitySingleton.Instance.UnityContainer) {Id = 123,Name = "Jimbo"}));
        }

        public void Clone_CloneObjectContainsListOfSimpleObjectsListItemWasRemoved_ObjectWasCloned()
        {
            ObjectBase_CloneTest<IObjectContainsListOfSimpleObjects>(11, obj => obj.SimpleObjects.RemoveAt(0));
        }

     
        [TestMethod]
        public void Clone_CloneDeriveFromSimpleObject_ObjectWasCloned()
        {
            ObjectBase_CloneTest<IDeriveFromSimpleObject>(11,obj => obj.ExtProperty = "ext property");
        }



        [TestMethod]
        public void Clone_CloneObjectContainsCircularReference_ObjectWasCloned()
        {
            IObjectContainsCircularReference objectContainsCircularReference = CreateObjectContainsCirculreReference();

            IDtoInstanceCreator dtoCreator = UnitySingleton.Instance.UnityContainer.Resolve<IDtoInstanceCreator>();
            ISelfTrackingDataTransferObject<IObjectContainsCircularReference> dtoObject = dtoCreator.CreateDtoInstance<IObjectContainsCircularReference>(objectContainsCircularReference);

            dtoObject.Populate(objectContainsCircularReference);

            IObjectContainsCircularReference cloneObj = dtoObject.Clone(false);

            Assert.AreEqual(cloneObj.InnerObjectContainsCircularReference.InnerObjectContainsCircularReference, cloneObj);
        }

        #region Helper methods

        private void ObjectBase_CloneTest<T>(int id,Action<T> modifyDto) where T : IObjectBase
        {
            T concreteObject = CreateConcreteType<T>(id);
            IDtoInstanceCreator dtoCreator = UnitySingleton.Instance.UnityContainer.Resolve<IDtoInstanceCreator>();
            ISelfTrackingDataTransferObject<T> dtoObject = dtoCreator.CreateDtoInstance<T>(concreteObject);
            dtoObject.Populate(concreteObject);

            T cloneObj = dtoObject.Clone(true);

            Assert.IsTrue(IntrefaceEqual<T>(concreteObject, cloneObj));

        }

        private void ObjectBase_ApplayChangesTest<T>(int id,Action<T> modifyDto) where T : IObjectBase
        {
            T concreteObject = CreateConcreteType<T>(id);
            IDtoInstanceCreator dtoCreator = UnitySingleton.Instance.UnityContainer.Resolve<IDtoInstanceCreator>();
            ISelfTrackingDataTransferObject<T> dtoObject = dtoCreator.CreateDtoInstance<T>(concreteObject);
            dtoObject.Populate(concreteObject);

            modifyDto((T)dtoObject);
            dtoObject.ApplyChanges(concreteObject);

            Assert.IsTrue(IntrefaceEqual<T>(dtoObject, concreteObject));

        }

        private void ObjectBase_PopulateTest<T>(int id) where T : IObjectBase
        {
            T objectContainsSimpleObject = CreateConcreteType<T>(id);
            IDtoInstanceCreator dtoCreator = UnitySingleton.Instance.UnityContainer.Resolve<IDtoInstanceCreator>();
            ISelfTrackingDataTransferObject<T> dtoObject = dtoCreator.CreateDtoInstance<T>(objectContainsSimpleObject);
            dtoObject.Populate(objectContainsSimpleObject);

            Assert.IsTrue(IntrefaceEqual<T>(dtoObject, objectContainsSimpleObject));

        }

        private static IObjectContainsCircularReference CreateObjectContainsCirculreReference()
        {
            IObjectContainsCircularReference objectContainsCircularReference = UnitySingleton.Instance.UnityContainer.Resolve<IObjectContainsCircularReference>();

            IObjectContainsCircularReference innerObjectContainsCircularReference = UnitySingleton.Instance.UnityContainer.Resolve<IObjectContainsCircularReference>();

            objectContainsCircularReference.Id = 111;
            innerObjectContainsCircularReference.Id = 222;

            objectContainsCircularReference.InnerObjectContainsCircularReference = innerObjectContainsCircularReference;
            innerObjectContainsCircularReference.InnerObjectContainsCircularReference = objectContainsCircularReference;
            return objectContainsCircularReference;
        }

        

        private bool IntrefaceEqual<T>(object dtoObject, object concreteObject)
        {
            PropertyInfo[] props = typeof(T).GetProperties();

            foreach (var pi in props)
            {
                if (pi.PropertyType.IsGenericType && typeof(IList<>) == pi.PropertyType.GetGenericTypeDefinition())
                {
                    if (!ListInterfaceEquals(pi, dtoObject, concreteObject))
                        return false;
                }
                else if (pi.PropertyType.GetInterfaces().Contains(typeof(IObjectBase)))
                {
                    if (!ObjectBaseEqual(pi, dtoObject, concreteObject))
                        return false;
                }
                //simple property
                else
                {
                    if (!SimpleEqual(pi, dtoObject, concreteObject))
                        return false;
                }
            }

            return true;
        }

        private bool ListInterfaceEquals(PropertyInfo pi, object dtoObject, object concreteObject)
        {
            PropertyInfo dtoPi = dtoObject.GetType().GetProperty(pi.Name);
            PropertyInfo concretePi = concreteObject.GetType().GetProperty(pi.Name);

            IList dtoList = dtoPi.GetValue(dtoObject, null) as IList;
            IList concreteList = concretePi.GetValue(concreteObject, null) as IList;

            if (dtoList == null && concreteList == null)
            {
                return true;
            }
            if (dtoList == null || concreteList == null)
            {
                return false;
            }

            if (dtoList.Count != concreteList.Count)
            {
                return false;
            }

            for (int i = 0; i < dtoList.Count ; i++)
            {
                if (!IntrefaceNonGenericEqual(pi.PropertyType.GetGenericArguments()[0], dtoList[i], concreteList[i]))
                    return false;
            }

            return true;


        }

        private bool IntrefaceNonGenericEqual(Type type, object dtoObject, object concreteObject)
        {
            MethodInfo mi = this.GetType().GetMethod("IntrefaceEqual", BindingFlags.Instance | BindingFlags.NonPublic).MakeGenericMethod(type);
            return (bool)mi.Invoke(this, new object[] { dtoObject, concreteObject });
        }

        private bool ObjectBaseEqual(PropertyInfo pi, object dtoObject, object concreteObject)
        {
            MethodInfo mi = this.GetType().GetMethod("IntrefaceEqual", BindingFlags.Instance | BindingFlags.NonPublic).MakeGenericMethod(pi.PropertyType);

            PropertyInfo dtoPi = dtoObject.GetType().GetProperty(pi.Name);
            PropertyInfo concretePi = concreteObject.GetType().GetProperty(pi.Name);

            return (bool)mi.Invoke(this, new object[] { dtoPi.GetValue(dtoObject, null), concretePi.GetValue(concreteObject, null) });
        }

        private bool SimpleEqual(PropertyInfo pi, object dtoObject, object concreteObject)
        {
            PropertyInfo dtoPi = dtoObject.GetType().GetProperty(pi.Name);
            PropertyInfo concretePi = concreteObject.GetType().GetProperty(pi.Name);

            return dtoPi.GetValue(dtoObject, null).Equals(concretePi.GetValue(concreteObject, null));
        }



        private T CreateConcreteType<T>(int id) where T: IObjectBase
        {
            T res = UnitySingleton.Instance.UnityContainer.Resolve<T>();

            res.Id = id;

            InitObjectProperties(res);

            return res;
        }

        private void InitObjectProperties<T>(T res)
        {
            PropertyInfo[] props = res.GetType().GetProperties();

            foreach (var pi in props)
            {
                if (pi.Name == "Id")
                {
                    continue;
                }
                if (pi.PropertyType.IsGenericType &&  typeof(IList<>) == pi.PropertyType.GetGenericTypeDefinition())
                {
                    SetListProperty(pi, res);
                }
                else if (pi.PropertyType.GetInterfaces().Contains(typeof(IObjectBase)))
                {
                    SetObjectProperty(pi, res);
                }
                //simple property
                else
                {
                    SetSimpleProperty(pi, res);
                }
                
            }

        }

        private void SetListProperty(PropertyInfo pi, object res)
        {
            IList newList = CreateNewList(pi);

            int maxNumOfItems = _rand.Next(1,5);

            for (int i = 0; i < maxNumOfItems; i++)
            {
                newList.Add(CreateNonGenericConcreteType(i, pi.PropertyType.GetGenericArguments()[0]));
            }

            pi.SetValue(res, newList,null);
        }

        private IList CreateNewList(PropertyInfo pi)
        {
            Type listType = typeof(List<>).MakeGenericType(pi.PropertyType.GetGenericArguments()[0]);
            object list = Activator.CreateInstance(listType);
            return (IList)list;
        }

        private object CreateNonGenericConcreteType(int id, Type type)
        {
            MethodInfo mi = this.GetType().GetMethod("CreateConcreteType", BindingFlags.Instance | BindingFlags.NonPublic).MakeGenericMethod(type);
            return mi.Invoke(this, new object[] { _rand.Next(100) });
        }

        private void SetObjectProperty(PropertyInfo pi, object res)
        {
            MethodInfo mi = this.GetType().GetMethod("CreateConcreteType", BindingFlags.Instance | BindingFlags.NonPublic).MakeGenericMethod(pi.PropertyType);
            object newValue = mi.Invoke(this, new object[] { _rand.Next(100)});
            pi.SetValue(res, newValue, null);
        }

        private void SetSimpleProperty(PropertyInfo pi, object res)
        {
            object newValue = Convert.ChangeType(_rand.Next(),pi.PropertyType);
            pi.SetValue(res, newValue, null);
        }

        #endregion
    }
}
